1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.solr.request;
19  
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.lucene.index.Fields;
22  import org.apache.lucene.index.LeafReader;
23  import org.apache.lucene.index.LeafReaderContext;
24  import org.apache.lucene.index.MultiPostingsEnum;
25  import org.apache.lucene.index.PostingsEnum;
26  import org.apache.lucene.index.Term;
27  import org.apache.lucene.index.Terms;
28  import org.apache.lucene.index.TermsEnum;
29  import org.apache.lucene.search.BooleanClause.Occur;
30  import org.apache.lucene.search.BooleanQuery;
31  import org.apache.lucene.search.DocIdSetIterator;
32  import org.apache.lucene.search.Filter;
33  import org.apache.lucene.search.FilterCollector;
34  import org.apache.lucene.search.FilteredQuery;
35  import org.apache.lucene.search.LeafCollector;
36  import org.apache.lucene.search.MatchAllDocsQuery;
37  import org.apache.lucene.search.Query;
38  import org.apache.lucene.search.TermQuery;
39  import org.apache.lucene.search.grouping.AbstractAllGroupHeadsCollector;
40  import org.apache.lucene.search.grouping.term.TermAllGroupsCollector;
41  import org.apache.lucene.search.grouping.term.TermGroupFacetCollector;
42  import org.apache.lucene.util.BytesRef;
43  import org.apache.lucene.util.CharsRefBuilder;
44  import org.apache.lucene.util.StringHelper;
45  import org.apache.solr.common.SolrException;
46  import org.apache.solr.common.SolrException.ErrorCode;
47  import org.apache.solr.common.params.CommonParams;
48  import org.apache.solr.common.params.FacetParams;
49  import org.apache.solr.common.params.GroupParams;
50  import org.apache.solr.common.params.RequiredSolrParams;
51  import org.apache.solr.common.params.SolrParams;
52  import org.apache.solr.common.util.ExecutorUtil;
53  import org.apache.solr.common.util.NamedList;
54  import org.apache.solr.common.util.SimpleOrderedMap;
55  import org.apache.solr.common.util.StrUtils;
56  import org.apache.solr.handler.component.FacetComponent;
57  import org.apache.solr.handler.component.ResponseBuilder;
58  import org.apache.solr.handler.component.SpatialHeatmapFacets;
59  import org.apache.solr.request.IntervalFacets.FacetInterval;
60  import org.apache.solr.schema.BoolField;
61  import org.apache.solr.schema.FieldType;
62  import org.apache.solr.schema.IndexSchema;
63  import org.apache.solr.schema.SchemaField;
64  import org.apache.solr.schema.TrieField;
65  import org.apache.solr.search.BitDocSet;
66  import org.apache.solr.search.DocSet;
67  import org.apache.solr.search.Grouping;
68  import org.apache.solr.search.HashDocSet;
69  import org.apache.solr.search.Insanity;
70  import org.apache.solr.search.QParser;
71  import org.apache.solr.search.QueryParsing;
72  import org.apache.solr.search.SolrIndexSearcher;
73  import org.apache.solr.search.SortedIntDocSet;
74  import org.apache.solr.search.SyntaxError;
75  import org.apache.solr.search.grouping.GroupingSpecification;
76  import org.apache.solr.util.BoundedTreeSet;
77  import org.apache.solr.util.DefaultSolrThreadFactory;
78  
79  import java.io.IOException;
80  import java.util.ArrayList;
81  import java.util.Collection;
82  import java.util.Collections;
83  import java.util.IdentityHashMap;
84  import java.util.List;
85  import java.util.Map;
86  import java.util.concurrent.Callable;
87  import java.util.concurrent.ExecutionException;
88  import java.util.concurrent.Executor;
89  import java.util.concurrent.Future;
90  import java.util.concurrent.FutureTask;
91  import java.util.concurrent.RunnableFuture;
92  import java.util.concurrent.Semaphore;
93  import java.util.concurrent.SynchronousQueue;
94  import java.util.concurrent.TimeUnit;
95  
96  /**
97   * A class that generates simple Facet information for a request.
98   *
99   * More advanced facet implementations may compose or subclass this class 
100  * to leverage any of its functionality.
101  */
102 public class SimpleFacets {
103   
104   /** The main set of documents all facet counts should be relative to */
105   protected DocSet docsOrig;
106   /** Configuration params behavior should be driven by */
107   protected final SolrParams global;
108   /** Searcher to use for all calculations */
109   protected final SolrIndexSearcher searcher;
110   protected final SolrQueryRequest req;
111   protected final ResponseBuilder rb;
112 
113   // per-facet values
114   protected final static class ParsedParams {
115     final public SolrParams localParams; // localParams on this particular facet command
116     final public SolrParams params;      // local+original
117     final public SolrParams required;    // required version of params
118     final public String facetValue;      // the field to or query to facet on (minus local params)
119     final public DocSet docs;            // the base docset for this particular facet
120     final public String key;             // what name should the results be stored under
121     final public List<String> tags;      // the tags applied to this facet value
122     final public int threads;
123     
124     public ParsedParams(final SolrParams localParams, // localParams on this particular facet command
125                         final SolrParams params,      // local+original
126                         final SolrParams required,    // required version of params
127                         final String facetValue,      // the field to or query to facet on (minus local params)
128                         final DocSet docs,            // the base docset for this particular facet
129                         final String key,             // what name should the results be stored under
130                         final List<String> tags,
131                         final int threads) {
132       this.localParams = localParams;
133       this.params = params;
134       this.required = required;
135       this.facetValue = facetValue;
136       this.docs = docs;
137       this.key = key;
138       this.tags = tags;
139       this.threads = threads;
140     }
141     
142     public ParsedParams withDocs(final DocSet docs) {
143       return new ParsedParams(localParams, params, required, facetValue, docs, key, tags, threads);
144     }
145   }
146 
147   public SimpleFacets(SolrQueryRequest req,
148                       DocSet docs,
149                       SolrParams params) {
150     this(req,docs,params,null);
151   }
152 
153   public SimpleFacets(SolrQueryRequest req,
154                       DocSet docs,
155                       SolrParams params,
156                       ResponseBuilder rb) {
157     this.req = req;
158     this.searcher = req.getSearcher();
159     this.docsOrig = docs;
160     this.global = params;
161     this.rb = rb;
162   }
163 
164   /**
165    * Returns <code>true</code> if a String contains the given substring. Otherwise
166    * <code>false</code>.
167    *
168    * @param ref
169    *          the {@link String} to test
170    * @param substring
171    *          the substring to look for
172    * @param ignoreCase
173    *          whether the comparison should be case-insensitive
174    * @return Returns <code>true</code> iff the String contains the given substring.
175    *         Otherwise <code>false</code>.
176    */
177   public static boolean contains(String ref, String substring, boolean ignoreCase) {
178     if (ignoreCase)
179       return StringUtils.containsIgnoreCase(ref, substring);
180     return StringUtils.contains(ref, substring);
181   }
182 
183 
184   protected ParsedParams parseParams(String type, String param) throws SyntaxError, IOException {
185     SolrParams localParams = QueryParsing.getLocalParams(param, req.getParams());
186     DocSet docs = docsOrig;
187     String facetValue = param;
188     String key = param;
189     List<String> tags = Collections.emptyList();
190     int threads = -1;
191 
192     if (localParams == null) {
193       SolrParams params = global;
194       SolrParams required = new RequiredSolrParams(params);
195       return new ParsedParams(localParams, params, required, facetValue, docs, key, tags, threads);
196     }
197     
198     SolrParams params = SolrParams.wrapDefaults(localParams, global);
199     SolrParams required = new RequiredSolrParams(params);
200 
201     // remove local params unless it's a query
202     if (type != FacetParams.FACET_QUERY) { // TODO Cut over to an Enum here
203       facetValue = localParams.get(CommonParams.VALUE);
204     }
205 
206     // reset set the default key now that localParams have been removed
207     key = facetValue;
208 
209     // allow explicit set of the key
210     key = localParams.get(CommonParams.OUTPUT_KEY, key);
211 
212     String tagStr = localParams.get(CommonParams.TAG);
213     tags = tagStr == null ? Collections.<String>emptyList() : StrUtils.splitSmart(tagStr,',');
214 
215     String threadStr = localParams.get(CommonParams.THREADS);
216     if (threadStr != null) {
217       threads = Integer.parseInt(threadStr);
218     }
219 
220     // figure out if we need a new base DocSet
221     String excludeStr = localParams.get(CommonParams.EXCLUDE);
222     if (excludeStr == null) return new ParsedParams(localParams, params, required, facetValue, docs, key, tags, threads);
223 
224     List<String> excludeTagList = StrUtils.splitSmart(excludeStr,',');
225     docs = computeDocSet(docs, excludeTagList);
226     return new ParsedParams(localParams, params, required, facetValue, docs, key, tags, threads);
227   }
228 
229   protected DocSet computeDocSet(DocSet baseDocSet, List<String> excludeTagList) throws SyntaxError, IOException {
230     Map<?,?> tagMap = (Map<?,?>)req.getContext().get("tags");
231     // rb can be null if facets are being calculated from a RequestHandler e.g. MoreLikeThisHandler
232     if (tagMap == null || rb == null) {
233       return baseDocSet;
234     }
235 
236     IdentityHashMap<Query,Boolean> excludeSet = new IdentityHashMap<>();
237     for (String excludeTag : excludeTagList) {
238       Object olst = tagMap.get(excludeTag);
239       // tagMap has entries of List<String,List<QParser>>, but subject to change in the future
240       if (!(olst instanceof Collection)) continue;
241       for (Object o : (Collection<?>)olst) {
242         if (!(o instanceof QParser)) continue;
243         QParser qp = (QParser)o;
244         excludeSet.put(qp.getQuery(), Boolean.TRUE);
245       }
246     }
247     if (excludeSet.size() == 0) return baseDocSet;
248 
249     List<Query> qlist = new ArrayList<>();
250 
251     // add the base query
252     if (!excludeSet.containsKey(rb.getQuery())) {
253       qlist.add(rb.getQuery());
254     }
255 
256     // add the filters
257     if (rb.getFilters() != null) {
258       for (Query q : rb.getFilters()) {
259         if (!excludeSet.containsKey(q)) {
260           qlist.add(q);
261         }
262       }
263     }
264 
265     // get the new base docset for this facet
266     DocSet base = searcher.getDocSet(qlist);
267     if (rb.grouping() && rb.getGroupingSpec().isTruncateGroups()) {
268       Grouping grouping = new Grouping(searcher, null, rb.getQueryCommand(), false, 0, false);
269       grouping.setWithinGroupSort(rb.getGroupingSpec().getSortWithinGroup());
270       if (rb.getGroupingSpec().getFields().length > 0) {
271         grouping.addFieldCommand(rb.getGroupingSpec().getFields()[0], req);
272       } else if (rb.getGroupingSpec().getFunctions().length > 0) {
273         grouping.addFunctionCommand(rb.getGroupingSpec().getFunctions()[0], req);
274       } else {
275         return base;
276       }
277       AbstractAllGroupHeadsCollector allGroupHeadsCollector = grouping.getCommands().get(0).createAllGroupCollector();
278       searcher.search(new FilteredQuery(new MatchAllDocsQuery(), base.getTopFilter()), allGroupHeadsCollector);
279       return new BitDocSet(allGroupHeadsCollector.retrieveGroupHeads(searcher.maxDoc()));
280     } else {
281       return base;
282     }
283   }
284 
285   /**
286    * Looks at various Params to determining if any simple Facet Constraint count
287    * computations are desired.
288    *
289    * @return a NamedList of Facet Count info or null
290    * @deprecated use {@link org.apache.solr.handler.component.FacetComponent#getFacetCounts(SimpleFacets)} instead
291    */
292   @Deprecated
293   public NamedList<Object> getFacetCounts() {
294     return FacetComponent.getFacetCounts(this);
295   }
296 
297   /**
298    * Returns a list of facet counts for each of the facet queries
299    * specified in the params
300    *
301    * @see FacetParams#FACET_QUERY
302    */
303   public NamedList<Integer> getFacetQueryCounts() throws IOException,SyntaxError {
304 
305     NamedList<Integer> res = new SimpleOrderedMap<>();
306 
307     /* Ignore CommonParams.DF - could have init param facet.query assuming
308      * the schema default with query param DF intented to only affect Q.
309      * If user doesn't want schema default for facet.query, they should be
310      * explicit.
311      */
312     // SolrQueryParser qp = searcher.getSchema().getSolrQueryParser(null);
313 
314     String[] facetQs = global.getParams(FacetParams.FACET_QUERY);
315 
316     if (null != facetQs && 0 != facetQs.length) {
317       for (String q : facetQs) {
318         final ParsedParams parsed = parseParams(FacetParams.FACET_QUERY, q);
319         getFacetQueryCount(parsed, res);
320       }
321     }
322 
323     return res;
324   }
325 
326   public void getFacetQueryCount(ParsedParams parsed, NamedList<Integer> res) throws SyntaxError, IOException {
327     // TODO: slight optimization would prevent double-parsing of any localParams
328     // TODO: SOLR-7753
329     Query qobj = QParser.getParser(parsed.facetValue, null, req).getQuery();
330 
331     if (qobj == null) {
332       res.add(parsed.key, 0);
333     } else if (parsed.params.getBool(GroupParams.GROUP_FACET, false)) {
334       res.add(parsed.key, getGroupedFacetQueryCount(qobj, parsed.docs));
335     } else {
336       res.add(parsed.key, searcher.numDocs(qobj, parsed.docs));
337     }
338   }
339 
340   /**
341    * Returns a grouped facet count for the facet query
342    *
343    * @see FacetParams#FACET_QUERY
344    */
345   public int getGroupedFacetQueryCount(Query facetQuery, DocSet docSet) throws IOException {
346     // It is okay to retrieve group.field from global because it is never a local param
347     String groupField = global.get(GroupParams.GROUP_FIELD);
348     if (groupField == null) {
349       throw new SolrException (
350           SolrException.ErrorCode.BAD_REQUEST,
351           "Specify the group.field as parameter or local parameter"
352       );
353     }
354 
355     TermAllGroupsCollector collector = new TermAllGroupsCollector(groupField);
356     Filter mainQueryFilter = docSet.getTopFilter(); // This returns a filter that only matches documents matching with q param and fq params
357     searcher.search(new FilteredQuery(facetQuery, mainQueryFilter), collector);
358     return collector.getGroupCount();
359   }
360 
361   enum FacetMethod {
362     ENUM, FC, FCS;
363   }
364 
365   /**
366    * Term counts for use in pivot faceting that resepcts the appropriate mincount
367    * @see FacetParams#FACET_PIVOT_MINCOUNT
368    */
369   public NamedList<Integer> getTermCountsForPivots(String field, ParsedParams parsed) throws IOException {
370     Integer mincount = parsed.params.getFieldInt(field, FacetParams.FACET_PIVOT_MINCOUNT, 1);
371     return getTermCounts(field, mincount, parsed);
372   }
373 
374   /**
375    * Term counts for use in field faceting that resepects the appropriate mincount
376    *
377    * @see FacetParams#FACET_MINCOUNT
378    */
379   public NamedList<Integer> getTermCounts(String field, ParsedParams parsed) throws IOException {
380     Integer mincount = parsed.params.getFieldInt(field, FacetParams.FACET_MINCOUNT);
381     return getTermCounts(field, mincount, parsed);
382   }
383 
384   /**
385    * Term counts for use in field faceting that resepcts the specified mincount - 
386    * if mincount is null, the "zeros" param is consulted for the appropriate backcompat 
387    * default
388    *
389    * @see FacetParams#FACET_ZEROS
390    */
391   private NamedList<Integer> getTermCounts(String field, Integer mincount, ParsedParams parsed) throws IOException {
392     final SolrParams params = parsed.params;
393     final DocSet docs = parsed.docs;
394     final int threads = parsed.threads;
395     int offset = params.getFieldInt(field, FacetParams.FACET_OFFSET, 0);
396     int limit = params.getFieldInt(field, FacetParams.FACET_LIMIT, 100);
397     if (limit == 0) return new NamedList<>();
398     if (mincount==null) {
399       Boolean zeros = params.getFieldBool(field, FacetParams.FACET_ZEROS);
400       // mincount = (zeros!=null && zeros) ? 0 : 1;
401       mincount = (zeros!=null && !zeros) ? 1 : 0;
402       // current default is to include zeros.
403     }
404     boolean missing = params.getFieldBool(field, FacetParams.FACET_MISSING, false);
405     // default to sorting if there is a limit.
406     String sort = params.getFieldParam(field, FacetParams.FACET_SORT, limit>0 ? FacetParams.FACET_SORT_COUNT : FacetParams.FACET_SORT_INDEX);
407     String prefix = params.getFieldParam(field, FacetParams.FACET_PREFIX);
408     String contains = params.getFieldParam(field, FacetParams.FACET_CONTAINS);
409     boolean ignoreCase = params.getFieldBool(field, FacetParams.FACET_CONTAINS_IGNORE_CASE, false);
410 
411     NamedList<Integer> counts;
412     SchemaField sf = searcher.getSchema().getField(field);
413     FieldType ft = sf.getType();
414 
415     // determine what type of faceting method to use
416     final String methodStr = params.getFieldParam(field, FacetParams.FACET_METHOD);
417     FacetMethod method = null;
418     if (FacetParams.FACET_METHOD_enum.equals(methodStr)) {
419       method = FacetMethod.ENUM;
420     } else if (FacetParams.FACET_METHOD_fcs.equals(methodStr)) {
421       method = FacetMethod.FCS;
422     } else if (FacetParams.FACET_METHOD_fc.equals(methodStr)) {
423       method = FacetMethod.FC;
424     }
425 
426     if (method == FacetMethod.ENUM && TrieField.getMainValuePrefix(ft) != null) {
427       // enum can't deal with trie fields that index several terms per value
428       method = sf.multiValued() ? FacetMethod.FC : FacetMethod.FCS;
429     }
430 
431     if (method == null && ft instanceof BoolField) {
432       // Always use filters for booleans... we know the number of values is very small.
433       method = FacetMethod.ENUM;
434     }
435 
436     final boolean multiToken = sf.multiValued() || ft.multiValuedFieldCache();
437     
438     if (ft.getNumericType() != null && !sf.multiValued()) {
439       // the per-segment approach is optimal for numeric field types since there
440       // are no global ords to merge and no need to create an expensive
441       // top-level reader
442       method = FacetMethod.FCS;
443     }
444 
445     if (method == null) {
446       // TODO: default to per-segment or not?
447       method = FacetMethod.FC;
448     }
449 
450     if (method == FacetMethod.FCS && multiToken) {
451       // only fc knows how to deal with multi-token fields
452       method = FacetMethod.FC;
453     }
454     
455     if (method == FacetMethod.ENUM && sf.hasDocValues()) {
456       // only fc can handle docvalues types
457       method = FacetMethod.FC;
458     }
459 
460     if (params.getFieldBool(field, GroupParams.GROUP_FACET, false)) {
461       counts = getGroupedCounts(searcher, docs, field, multiToken, offset,limit, mincount, missing, sort, prefix, contains, ignoreCase);
462     } else {
463       assert method != null;
464       switch (method) {
465         case ENUM:
466           assert TrieField.getMainValuePrefix(ft) == null;
467           counts = getFacetTermEnumCounts(searcher, docs, field, offset, limit, mincount,missing,sort,prefix, contains, ignoreCase, params);
468           break;
469         case FCS:
470           assert !multiToken;
471           if (ft.getNumericType() != null && !sf.multiValued()) {
472             // force numeric faceting
473             if (prefix != null && !prefix.isEmpty()) {
474               throw new SolrException(ErrorCode.BAD_REQUEST, FacetParams.FACET_PREFIX + " is not supported on numeric types");
475             }
476             if (contains != null && !contains.isEmpty()) {
477               throw new SolrException(ErrorCode.BAD_REQUEST, FacetParams.FACET_CONTAINS + " is not supported on numeric types");
478             }
479             counts = NumericFacets.getCounts(searcher, docs, field, offset, limit, mincount, missing, sort);
480           } else {
481             PerSegmentSingleValuedFaceting ps = new PerSegmentSingleValuedFaceting(searcher, docs, field, offset, limit, mincount, missing, sort, prefix, contains, ignoreCase);
482             Executor executor = threads == 0 ? directExecutor : facetExecutor;
483             ps.setNumThreads(threads);
484             counts = ps.getFacetCounts(executor);
485           }
486           break;
487         case FC:
488           counts = DocValuesFacets.getCounts(searcher, docs, field, offset,limit, mincount, missing, sort, prefix, contains, ignoreCase);
489           break;
490         default:
491           throw new AssertionError();
492       }
493     }
494 
495     return counts;
496   }
497 
498   public NamedList<Integer> getGroupedCounts(SolrIndexSearcher searcher,
499                                              DocSet base,
500                                              String field,
501                                              boolean multiToken,
502                                              int offset,
503                                              int limit,
504                                              int mincount,
505                                              boolean missing,
506                                              String sort,
507                                              String prefix,
508                                              String contains,
509                                              boolean ignoreCase) throws IOException {
510     GroupingSpecification groupingSpecification = rb.getGroupingSpec();
511     final String groupField  = groupingSpecification != null ? groupingSpecification.getFields()[0] : null;
512     if (groupField == null) {
513       throw new SolrException (
514           SolrException.ErrorCode.BAD_REQUEST,
515           "Specify the group.field as parameter or local parameter"
516       );
517     }
518 
519     BytesRef prefixBytesRef = prefix != null ? new BytesRef(prefix) : null;
520     final TermGroupFacetCollector collector = TermGroupFacetCollector.createTermGroupFacetCollector(groupField, field, multiToken, prefixBytesRef, 128);
521     
522     SchemaField sf = searcher.getSchema().getFieldOrNull(groupField);
523     
524     if (sf != null && sf.hasDocValues() == false && sf.multiValued() == false && sf.getType().getNumericType() != null) {
525       // it's a single-valued numeric field: we must currently create insanity :(
526       // there isn't a GroupedFacetCollector that works on numerics right now...
527       searcher.search(base.getTopFilter(), new FilterCollector(collector) {
528         @Override
529         public LeafCollector getLeafCollector(LeafReaderContext context) throws IOException {
530           LeafReader insane = Insanity.wrapInsanity(context.reader(), groupField);
531           return in.getLeafCollector(insane.getContext());
532         }
533       });
534     } else {
535       searcher.search(base.getTopFilter(), collector);
536     }
537     
538     boolean orderByCount = sort.equals(FacetParams.FACET_SORT_COUNT) || sort.equals(FacetParams.FACET_SORT_COUNT_LEGACY);
539     TermGroupFacetCollector.GroupedFacetResult result 
540       = collector.mergeSegmentResults(limit < 0 ? Integer.MAX_VALUE : 
541                                       (offset + limit), 
542                                       mincount, orderByCount);
543 
544     CharsRefBuilder charsRef = new CharsRefBuilder();
545     FieldType facetFieldType = searcher.getSchema().getFieldType(field);
546     NamedList<Integer> facetCounts = new NamedList<>();
547     List<TermGroupFacetCollector.FacetEntry> scopedEntries 
548       = result.getFacetEntries(offset, limit < 0 ? Integer.MAX_VALUE : limit);
549     for (TermGroupFacetCollector.FacetEntry facetEntry : scopedEntries) {
550       //:TODO:can we do contains earlier than this to make it more efficient?
551       if (contains != null && !contains(facetEntry.getValue().utf8ToString(), contains, ignoreCase)) {
552         continue;
553       }
554       facetFieldType.indexedToReadable(facetEntry.getValue(), charsRef);
555       facetCounts.add(charsRef.toString(), facetEntry.getCount());
556     }
557 
558     if (missing) {
559       facetCounts.add(null, result.getTotalMissingCount());
560     }
561 
562     return facetCounts;
563   }
564 
565 
566   static final Executor directExecutor = new Executor() {
567     @Override
568     public void execute(Runnable r) {
569       r.run();
570     }
571   };
572 
573   static final Executor facetExecutor = new ExecutorUtil.MDCAwareThreadPoolExecutor(
574           0,
575           Integer.MAX_VALUE,
576           10, TimeUnit.SECONDS, // terminate idle threads after 10 sec
577           new SynchronousQueue<Runnable>()  // directly hand off tasks
578           , new DefaultSolrThreadFactory("facetExecutor")
579   );
580   
581   /**
582    * Returns a list of value constraints and the associated facet counts 
583    * for each facet field specified in the params.
584    *
585    * @see FacetParams#FACET_FIELD
586    * @see #getFieldMissingCount
587    * @see #getFacetTermEnumCounts
588    */
589   @SuppressWarnings("unchecked")
590   public NamedList<Object> getFacetFieldCounts()
591       throws IOException, SyntaxError {
592 
593     NamedList<Object> res = new SimpleOrderedMap<>();
594     String[] facetFs = global.getParams(FacetParams.FACET_FIELD);
595     if (null == facetFs) {
596       return res;
597     }
598 
599     // Passing a negative number for FACET_THREADS implies an unlimited number of threads is acceptable.
600     // Also, a subtlety of directExecutor is that no matter how many times you "submit" a job, it's really
601     // just a method call in that it's run by the calling thread.
602     int maxThreads = req.getParams().getInt(FacetParams.FACET_THREADS, 0);
603     Executor executor = maxThreads == 0 ? directExecutor : facetExecutor;
604     final Semaphore semaphore = new Semaphore((maxThreads <= 0) ? Integer.MAX_VALUE : maxThreads);
605     List<Future<NamedList>> futures = new ArrayList<>(facetFs.length);
606 
607     try {
608       //Loop over fields; submit to executor, keeping the future
609       for (String f : facetFs) {
610         final ParsedParams parsed = parseParams(FacetParams.FACET_FIELD, f);
611         final SolrParams localParams = parsed.localParams;
612         final String termList = localParams == null ? null : localParams.get(CommonParams.TERMS);
613         final String key = parsed.key;
614         final String facetValue = parsed.facetValue;
615         Callable<NamedList> callable = new Callable<NamedList>() {
616           @Override
617           public NamedList call() throws Exception {
618             try {
619               NamedList<Object> result = new SimpleOrderedMap<>();
620               if(termList != null) {
621                 List<String> terms = StrUtils.splitSmart(termList, ",", true);
622                 result.add(key, getListedTermCounts(facetValue, parsed, terms));
623               } else {
624                 result.add(key, getTermCounts(facetValue, parsed));
625               }
626               return result;
627             } catch (SolrException se) {
628               throw se;
629             } catch (Exception e) {
630               throw new SolrException(ErrorCode.SERVER_ERROR,
631                                       "Exception during facet.field: " + facetValue, e);
632             } finally {
633               semaphore.release();
634             }
635           }
636         };
637 
638         RunnableFuture<NamedList> runnableFuture = new FutureTask<>(callable);
639         semaphore.acquire();//may block and/or interrupt
640         executor.execute(runnableFuture);//releases semaphore when done
641         futures.add(runnableFuture);
642       }//facetFs loop
643 
644       //Loop over futures to get the values. The order is the same as facetFs but shouldn't matter.
645       for (Future<NamedList> future : futures) {
646         res.addAll(future.get());
647       }
648       assert semaphore.availablePermits() >= maxThreads;
649     } catch (InterruptedException e) {
650       throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
651           "Error while processing facet fields: InterruptedException", e);
652     } catch (ExecutionException ee) {
653       Throwable e = ee.getCause();//unwrap
654       if (e instanceof RuntimeException) {
655         throw (RuntimeException) e;
656       }
657       throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
658           "Error while processing facet fields: " + e.toString(), e);
659     }
660 
661     return res;
662   }
663 
664   /**
665    * Computes the term-&gt;count counts for the specified term values relative to the 
666    * @param field the name of the field to compute term counts against
667    * @param parsed contains the docset to compute term counts relative to
668    * @param terms a list of term values (in the specified field) to compute the counts for 
669    */
670   protected NamedList<Integer> getListedTermCounts(String field, final ParsedParams parsed, List<String> terms) throws IOException {
671     FieldType ft = searcher.getSchema().getFieldType(field);
672     NamedList<Integer> res = new NamedList<>();
673     for (String term : terms) {
674       String internal = ft.toInternal(term);
675       int count = searcher.numDocs(new TermQuery(new Term(field, internal)), parsed.docs);
676       res.add(term, count);
677     }
678     return res;    
679   }
680 
681 
682   /**
683    * Returns a count of the documents in the set which do not have any 
684    * terms for for the specified field.
685    *
686    * @see FacetParams#FACET_MISSING
687    */
688   public static int getFieldMissingCount(SolrIndexSearcher searcher, DocSet docs, String fieldName)
689     throws IOException {
690     SchemaField sf = searcher.getSchema().getField(fieldName);
691     DocSet hasVal = searcher.getDocSet
692         (sf.getType().getRangeQuery(null, sf, null, null, false, false));
693     return docs.andNotSize(hasVal);
694   }
695 
696   /**
697    * Returns a list of terms in the specified field along with the 
698    * corresponding count of documents in the set that match that constraint.
699    * This method uses the FilterCache to get the intersection count between <code>docs</code>
700    * and the DocSet for each term in the filter.
701    *
702    * @see FacetParams#FACET_LIMIT
703    * @see FacetParams#FACET_ZEROS
704    * @see FacetParams#FACET_MISSING
705    */
706   public NamedList<Integer> getFacetTermEnumCounts(SolrIndexSearcher searcher, DocSet docs, String field, int offset, int limit, int mincount, boolean missing, String sort, String prefix, String contains, boolean ignoreCase, SolrParams params)
707     throws IOException {
708 
709     /* :TODO: potential optimization...
710     * cache the Terms with the highest docFreq and try them first
711     * don't enum if we get our max from them
712     */
713 
714     // Minimum term docFreq in order to use the filterCache for that term.
715     int minDfFilterCache = global.getFieldInt(field, FacetParams.FACET_ENUM_CACHE_MINDF, 0);
716 
717     // make sure we have a set that is fast for random access, if we will use it for that
718     DocSet fastForRandomSet = docs;
719     if (minDfFilterCache>0 && docs instanceof SortedIntDocSet) {
720       SortedIntDocSet sset = (SortedIntDocSet)docs;
721       fastForRandomSet = new HashDocSet(sset.getDocs(), 0, sset.size());
722     }
723 
724 
725     IndexSchema schema = searcher.getSchema();
726     LeafReader r = searcher.getLeafReader();
727     FieldType ft = schema.getFieldType(field);
728 
729     boolean sortByCount = sort.equals("count") || sort.equals("true");
730     final int maxsize = limit>=0 ? offset+limit : Integer.MAX_VALUE-1;
731     final BoundedTreeSet<CountPair<BytesRef,Integer>> queue = sortByCount ? new BoundedTreeSet<CountPair<BytesRef,Integer>>(maxsize) : null;
732     final NamedList<Integer> res = new NamedList<>();
733 
734     int min=mincount-1;  // the smallest value in the top 'N' values    
735     int off=offset;
736     int lim=limit>=0 ? limit : Integer.MAX_VALUE;
737 
738     BytesRef prefixTermBytes = null;
739     if (prefix != null) {
740       String indexedPrefix = ft.toInternal(prefix);
741       prefixTermBytes = new BytesRef(indexedPrefix);
742     }
743 
744     Fields fields = r.fields();
745     Terms terms = fields==null ? null : fields.terms(field);
746     TermsEnum termsEnum = null;
747     SolrIndexSearcher.DocsEnumState deState = null;
748     BytesRef term = null;
749     if (terms != null) {
750       termsEnum = terms.iterator();
751 
752       // TODO: OPT: if seek(ord) is supported for this termsEnum, then we could use it for
753       // facet.offset when sorting by index order.
754 
755       if (prefixTermBytes != null) {
756         if (termsEnum.seekCeil(prefixTermBytes) == TermsEnum.SeekStatus.END) {
757           termsEnum = null;
758         } else {
759           term = termsEnum.term();
760         }
761       } else {
762         // position termsEnum on first term
763         term = termsEnum.next();
764       }
765     }
766 
767     PostingsEnum postingsEnum = null;
768     CharsRefBuilder charsRef = new CharsRefBuilder();
769 
770     if (docs.size() >= mincount) {
771       while (term != null) {
772 
773         if (prefixTermBytes != null && !StringHelper.startsWith(term, prefixTermBytes))
774           break;
775 
776         if (contains == null || contains(term.utf8ToString(), contains, ignoreCase)) {
777           int df = termsEnum.docFreq();
778 
779           // If we are sorting, we can use df>min (rather than >=) since we
780           // are going in index order.  For certain term distributions this can
781           // make a large difference (for example, many terms with df=1).
782           if (df > 0 && df > min) {
783             int c;
784 
785             if (df >= minDfFilterCache) {
786               // use the filter cache
787 
788               if (deState == null) {
789                 deState = new SolrIndexSearcher.DocsEnumState();
790                 deState.fieldName = field;
791                 deState.liveDocs = r.getLiveDocs();
792                 deState.termsEnum = termsEnum;
793                 deState.postingsEnum = postingsEnum;
794               }
795 
796               c = searcher.numDocs(docs, deState);
797 
798               postingsEnum = deState.postingsEnum;
799             } else {
800               // iterate over TermDocs to calculate the intersection
801 
802               // TODO: specialize when base docset is a bitset or hash set (skipDocs)?  or does it matter for this?
803               // TODO: do this per-segment for better efficiency (MultiDocsEnum just uses base class impl)
804               // TODO: would passing deleted docs lead to better efficiency over checking the fastForRandomSet?
805               postingsEnum = termsEnum.postings(postingsEnum, PostingsEnum.NONE);
806               c = 0;
807 
808               if (postingsEnum instanceof MultiPostingsEnum) {
809                 MultiPostingsEnum.EnumWithSlice[] subs = ((MultiPostingsEnum) postingsEnum).getSubs();
810                 int numSubs = ((MultiPostingsEnum) postingsEnum).getNumSubs();
811                 for (int subindex = 0; subindex < numSubs; subindex++) {
812                   MultiPostingsEnum.EnumWithSlice sub = subs[subindex];
813                   if (sub.postingsEnum == null) continue;
814                   int base = sub.slice.start;
815                   int docid;
816                   while ((docid = sub.postingsEnum.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
817                     if (fastForRandomSet.exists(docid + base)) c++;
818                   }
819                 }
820               } else {
821                 int docid;
822                 while ((docid = postingsEnum.nextDoc()) != DocIdSetIterator.NO_MORE_DOCS) {
823                   if (fastForRandomSet.exists(docid)) c++;
824                 }
825               }
826 
827             }
828 
829             if (sortByCount) {
830               if (c > min) {
831                 BytesRef termCopy = BytesRef.deepCopyOf(term);
832                 queue.add(new CountPair<>(termCopy, c));
833                 if (queue.size() >= maxsize) min = queue.last().val;
834               }
835             } else {
836               if (c >= mincount && --off < 0) {
837                 if (--lim < 0) break;
838                 ft.indexedToReadable(term, charsRef);
839                 res.add(charsRef.toString(), c);
840               }
841             }
842           }
843         }
844         term = termsEnum.next();
845       }
846     }
847 
848     if (sortByCount) {
849       for (CountPair<BytesRef,Integer> p : queue) {
850         if (--off>=0) continue;
851         if (--lim<0) break;
852         ft.indexedToReadable(p.key, charsRef);
853         res.add(charsRef.toString(), p.val);
854       }
855     }
856 
857     if (missing) {
858       res.add(null, getFieldMissingCount(searcher,docs,field));
859     }
860 
861     return res;
862   }
863 
864   /**
865    * A simple key=&gt;val pair whose natural order is such that 
866    * <b>higher</b> vals come before lower vals.
867    * In case of tie vals, then <b>lower</b> keys come before higher keys.
868    */
869   public static class CountPair<K extends Comparable<? super K>, V extends Comparable<? super V>>
870     implements Comparable<CountPair<K,V>> {
871 
872     public CountPair(K k, V v) {
873       key = k; val = v;
874     }
875     public K key;
876     public V val;
877     @Override
878     public int hashCode() {
879       return key.hashCode() ^ val.hashCode();
880     }
881     @Override
882     public boolean equals(Object o) {
883       if (! (o instanceof CountPair)) return false;
884       CountPair<?,?> that = (CountPair<?,?>) o;
885       return (this.key.equals(that.key) && this.val.equals(that.val));
886     }
887     @Override
888     public int compareTo(CountPair<K,V> o) {
889       int vc = o.val.compareTo(val);
890       return (0 != vc ? vc : key.compareTo(o.key));
891     }
892   }
893 
894 
895   /**
896    * Returns a <code>NamedList</code> with each entry having the "key" of the interval as name and the count of docs 
897    * in that interval as value. All intervals added in the request are included in the returned 
898    * <code>NamedList</code> (included those with 0 count), and it's required that the order of the intervals
899    * is deterministic and equals in all shards of a distributed request, otherwise the collation of results
900    * will fail. 
901    * 
902    */
903   public NamedList<Object> getFacetIntervalCounts() throws IOException, SyntaxError {
904     NamedList<Object> res = new SimpleOrderedMap<Object>();
905     String[] fields = global.getParams(FacetParams.FACET_INTERVAL);
906     if (fields == null || fields.length == 0) return res;
907 
908     for (String field : fields) {
909       final ParsedParams parsed = parseParams(FacetParams.FACET_INTERVAL, field);
910       String[] intervalStrs = parsed.required.getFieldParams(parsed.facetValue, FacetParams.FACET_INTERVAL_SET);
911       SchemaField schemaField = searcher.getCore().getLatestSchema().getField(parsed.facetValue);
912       if (parsed.params.getBool(GroupParams.GROUP_FACET, false)) {
913         throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Interval Faceting can't be used with " + GroupParams.GROUP_FACET);
914       }
915       
916       SimpleOrderedMap<Integer> fieldResults = new SimpleOrderedMap<Integer>();
917       res.add(parsed.key, fieldResults);
918       IntervalFacets intervalFacets = new IntervalFacets(schemaField, searcher, parsed.docs, intervalStrs, parsed.params);
919       for (FacetInterval interval : intervalFacets) {
920         fieldResults.add(interval.getKey(), interval.getCount());
921       }
922     }
923 
924     return res;
925   }
926 
927   public NamedList getHeatmapCounts() throws IOException, SyntaxError {
928     final NamedList<Object> resOuter = new SimpleOrderedMap<>();
929     String[] unparsedFields = rb.req.getParams().getParams(FacetParams.FACET_HEATMAP);
930     if (unparsedFields == null || unparsedFields.length == 0) {
931       return resOuter;
932     }
933     if (global.getBool(GroupParams.GROUP_FACET, false)) {
934       throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
935           "Heatmaps can't be used with " + GroupParams.GROUP_FACET);
936     }
937     for (String unparsedField : unparsedFields) {
938       final ParsedParams parsed = parseParams(FacetParams.FACET_HEATMAP, unparsedField); // populates facetValue, rb, params, docs
939 
940       resOuter.add(parsed.key, SpatialHeatmapFacets.getHeatmapForField(parsed.key, parsed.facetValue, rb, parsed.params, parsed.docs));
941     }
942     return resOuter;
943   }
944 
945   public SolrParams getGlobalParams() {
946     return global;
947   }
948 
949   public DocSet getDocsOrig() {
950     return docsOrig;
951   }
952 
953   public SolrQueryRequest getRequest() {
954     return req;
955   }
956 
957   public ResponseBuilder getResponseBuilder() {
958     return rb;
959   }
960 }
961